package com.hsurosy.domain.strategy.service.armory;

import com.hsurosy.domain.strategy.model.entity.StrategyAwardEntity;
import com.hsurosy.domain.strategy.model.entity.StrategyEntity;
import com.hsurosy.domain.strategy.model.entity.StrategyRuleEntity;
import com.hsurosy.domain.strategy.repository.IStrategyRepository;
import com.hsurosy.types.common.Constants;
import com.hsurosy.types.enums.ResponseCode;
import com.hsurosy.types.exception.AppException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.security.SecureRandom;
import java.util.*;

/**
 * @Author Hsu琛君珩
 * @Date 2024-10-15 17:04
 * @Description 抽奖策略装配库，负责初始化策略计算
 * @Version: v1.0.0
 */
@Slf4j
@Service
public class StrategyArmoryDispatch implements IStrategyArmory, IStrategyDispatch {

    private final SecureRandom secureRandom = new SecureRandom(); // 用于生成随机数的安全随机数生成器

    @Resource
    private IStrategyRepository strategyRepository; // 策略数据仓储

    /**
     * 根据活动ID装配抽奖策略
     *
     * @param activityId 活动ID
     * @return 装配结果
     */
    @Override
    public boolean assembleLotteryStrategyByActivityId(Long activityId) {
        // 查询与活动关联的抽奖策略ID
        Long strategyId = strategyRepository.queryStrategyIdByActivityId(activityId);
        // 调用装配抽奖策略方法
        return assembleLotteryStrategy(strategyId);
    }

    /**
     * 装配抽奖策略，根据每个奖品的概率生成概率查找表，并将其存储在 Redis 中
     *
     * @param strategyId 抽奖策略ID，用于标识特定的抽奖策略
     * @return 返回 true 表示装配成功
     */
    @Override
    public boolean assembleLotteryStrategy(Long strategyId) {
        // 1. 查询策略ID对应的奖品列表，包含每个奖品的概率信息
        // 奖品A：ID = 1，概率 = 0.10，奖品B：ID = 2，概率 = 0.05，奖品C：ID = 3，概率 = 0.01
        List<StrategyAwardEntity> strategyAwardEntities = strategyRepository.queryStrategyAwardList(strategyId);

        // 2. 将每个奖品的库存信息缓存到 Redis 中【用于 decr 扣减库存使用】
        for (StrategyAwardEntity strategyAward : strategyAwardEntities) {
            Integer awardId = strategyAward.getAwardId();  // 奖品ID
            Integer awardCount = strategyAward.getAwardCount();  // 奖品库存
            cacheStrategyAwardCount(strategyId, awardId, awardCount);  // 将奖品库存缓存到 Redis 中
        }

        // 3.1 默认装配策略：为策略ID生成默认的概率查找表，并存储到 Redis 中
        assembleLotteryStrategy(String.valueOf(strategyId), strategyAwardEntities);  // 默认抽奖策略装配格式：100001

        // 3.2 权重策略装配：适用于权重规则配置的抽奖策略
        StrategyEntity strategyEntity = strategyRepository.queryStrategyEntityByStrategyId(strategyId);  // 查询策略实体
        String ruleWeight = strategyEntity.getRuleWeight();  // 获取权重规则的标识(也是模型) rule_weight

        // 如果没有定义权重规则，则直接返回 true 表示默认策略装配完成
        if (null == ruleWeight) {
            return true;
        }

        // 查询与权重规则相关的策略规则实体，获取具体的权重配置
        StrategyRuleEntity strategyRuleEntity = strategyRepository.queryStrategyRule(strategyId, ruleWeight);
        // 如果没有找到对应的权重规则，抛出异常提示
        if (null == strategyRuleEntity) {
            throw new AppException(ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getCode(), ResponseCode.STRATEGY_RULE_WEIGHT_IS_NULL.getInfo());
        }

        // 获取权重值映射表，格式为： 4000: [102,103,104]，5000: [102,103,104,105,106] 等
        Map<String, List<Integer>> ruleWeightValueMap = strategyRuleEntity.getRuleWeightValues();
        Set<String> keys = ruleWeightValueMap.keySet();  // 获取所有权重区间的 key，例如 4000、5000 等

        // 遍历每个权重区间的 key，装配该区间的抽奖策略，为每个权重区间生成查找表
        for (String key : keys) {
            List<Integer> ruleWeightValues = ruleWeightValueMap.get(key);  // 获取当前权重区间对应的奖品ID列表
            // 复制奖品列表并移除不在当前权重区间的奖品
            ArrayList<StrategyAwardEntity> strategyAwardEntitiesClone = new ArrayList<>(strategyAwardEntities);
            strategyAwardEntitiesClone.removeIf(entity -> !ruleWeightValues.contains(entity.getAwardId()));
            // 为当前权重区间装配抽奖策略，并存储到 Redis 中
            assembleLotteryStrategy(String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(key), strategyAwardEntitiesClone); // 权重装配抽奖策略格式：100001_4000、100001_5000
        }

        // 返回 true 表示装配完成
        return true;
    }

    /**
     * 将奖品数量缓存到 Redis 中
     *
     * @param strategyId 抽奖策略ID，用于标识是哪一个抽奖策略
     * @param awardId 奖品ID，用于标识是哪一个奖品
     * @param awardCount 奖品数量，表示该奖品当前的库存数量
     */
    private void cacheStrategyAwardCount(Long strategyId, Integer awardId, Integer awardCount) {
        // 1. 生成缓存的 key，格式为 "strategy_award_count_{strategyId}_{awardId}"
        //    cacheKey 的结构为：策略ID + "_" + 奖品ID，唯一标识每个策略下的不同奖品数量
        String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;

        // 2. 调用仓储层的方法，将奖品的数量（awardCount）缓存到 Redis 中，使用生成的 cacheKey 作为唯一标识
        strategyRepository.cacheStrategyAwardCount(cacheKey, awardCount);
    }

    /**
     * 根据奖品的概率生成概率查找表，并存储在Redis中
     *
     * @param key 抽奖策略的唯一标识
     * @param strategyAwardEntities 奖品列表，包含奖品ID和对应的概率
     */
    private void assembleLotteryStrategy(String key, List<StrategyAwardEntity> strategyAwardEntities) {
        // 1. 获取奖品列表中最小的奖品概率值（用于确定最小概率单位）
        // 比如 0.01
        BigDecimal minAwardRate = strategyAwardEntities.stream()
                .map(StrategyAwardEntity::getAwardRate)
                .min(BigDecimal::compareTo)
                .orElse(BigDecimal.ZERO);

        // 2. 用 1 * 1000 获得概率范围，百分位、千分位、万分位
        BigDecimal rateRange = BigDecimal.valueOf(convert(minAwardRate.doubleValue()));

        // 3. 生成策略奖品概率查找表「这里指需要在list集合中，存放上对应的奖品占位即可，占位越多等于概率越高」
        List<Integer> strategyAwardSearchRateTables = new ArrayList<>(rateRange.intValue());
        for (StrategyAwardEntity strategyAward : strategyAwardEntities) {
            Integer awardId = strategyAward.getAwardId(); // 奖品ID
            BigDecimal awardRate = strategyAward.getAwardRate(); // 中奖概率
            // 计算出每个概率值需要存放到查找表的数量，循环填充
            // 比如：奖品A的概率是 0.10，那么它在查找表中将会有 rateRange * 0.10 的位置被填充为奖品A的ID
            for (int i = 0; i < rateRange.multiply(awardRate).setScale(0, RoundingMode.CEILING).intValue(); i++) {
                strategyAwardSearchRateTables.add(awardId);
            }
            // 比如 查找表 = [A, A, B, C]
        }

        // 4. 将查找表打乱顺序，以保证抽奖的公平性
        // 比如 查找表 = [A, C, B, A]
        Collections.shuffle(strategyAwardSearchRateTables);

        // 5. 将打乱后的查找表转换为HashMap形式，方便查找
        // 比如 查找表映射 = {
        //          0 -> A,
        //          1 -> C,
        //          2 -> B,
        //          3 -> A
        //      }
        Map<Integer, Integer> shuffleStrategyAwardSearchRateTable = new LinkedHashMap<>();
        for (int i = 0; i < strategyAwardSearchRateTables.size(); i++) {
            shuffleStrategyAwardSearchRateTable.put(i, strategyAwardSearchRateTables.get(i));
        }

        // 6. 将最终的概率查找表和其大小存储到Redis缓存中
        strategyRepository.storeStrategyAwardSearchRateTable(key, shuffleStrategyAwardSearchRateTable.size(), shuffleStrategyAwardSearchRateTable);
    }

    /**
     * 转换计算，只根据小数位来计算。如【0.01返回100】、【0.009返回1000】、【0.0018返回10000】
     */
    private double convert(double min) {
        if (0 == min) {
            return 1D;
        }

        double current = min;
        double max = 1;
        while (current < 1) {
            current = current * 10;
            max = max * 10;
        }
        return max;
    }

    /**
     * 随机抽取奖品，基于策略ID生成一个随机值来决定抽中的奖品
     *
     * @param strategyId 抽奖策略ID
     * @return 抽中的奖品ID
     */
    @Override
    public Integer getRandomAwardId(Long strategyId) {
        // 1. 分布式部署下，不一定为当前应用做的策略装配，需从 Redis 中获取策略的概率范围
        int rateRange = strategyRepository.getRateRange(strategyId);
        // 2. 通过生成随机值，依据概率查找表来确定抽中的奖品
        return strategyRepository.getStrategyAwardAssemble(String.valueOf(strategyId), secureRandom.nextInt(rateRange)); // key：100001
    }

    /**
     * 根据策略ID和权重规则值随机抽取奖品
     *
     * @param strategyId 抽奖策略ID
     * @param ruleWeightValue 权重规则值
     * @return 抽中的奖品ID
     */
    @Override
    public Integer getRandomAwardId(Long strategyId, String ruleWeightValue) {
        // 1. 生成包含策略ID和权重规则的 Redis key
        String key = String.valueOf(strategyId).concat(Constants.UNDERLINE).concat(ruleWeightValue); // key：100001_4000
        // 2. 使用生成的 key 来随机抽取奖品
        return getRandomAwardId(key);
    }

    /**
     * 根据给定的 Redis key 随机抽取奖品
     * 该方法与上一个方法类似，只是通过具体的 Redis key 直接操作。
     *
     * @param key Redis 中存储策略信息的 key
     * @return 抽中的奖品ID
     */
    @Override
    public Integer getRandomAwardId(String key) {
        // 1. 从 Redis 中获取策略的概率范围
        int rateRange = strategyRepository.getRateRange(key);
        // 2. 通过生成随机值，依据概率查找表来确定抽中的奖品
        return strategyRepository.getStrategyAwardAssemble(key, secureRandom.nextInt(rateRange));
    }

    /**
     * 扣减指定奖品的库存
     *
     * @param strategyId 抽奖策略ID
     * @param awardId 奖品ID
     * @param endDateTime 活动结束时间
     * @return 如果库存扣减成功，返回 true；否则返回 false
     */
    @Override
    public Boolean subtractionAwardStock(Long strategyId, Integer awardId, Date endDateTime) {
        // 1. 生成 Redis 缓存的 key，用于存储奖品库存
        String cacheKey = Constants.RedisKey.STRATEGY_AWARD_COUNT_KEY + strategyId + Constants.UNDERLINE + awardId;
        // 2. 从 Redis 中扣减奖品库存
        return strategyRepository.subtractionAwardStock(cacheKey, endDateTime);
    }

}
